/**
 * 
 */
package cz.cuni.mff.abacs.burglar.logics;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import aStarLibrary.AStar;
import aStarLibrary.Node;
import cz.cuni.mff.abacs.burglar.logics.objects.BaseInterface;
import cz.cuni.mff.abacs.burglar.logics.objects.Room;
import cz.cuni.mff.abacs.burglar.logics.objects.agents.Agent;
import cz.cuni.mff.abacs.burglar.logics.objects.agents.BeliefBase;
import cz.cuni.mff.abacs.burglar.logics.objects.agents.Guard;
import cz.cuni.mff.abacs.burglar.logics.objects.items.Inventory;
import cz.cuni.mff.abacs.burglar.logics.objects.items.Item;
import cz.cuni.mff.abacs.burglar.logics.objects.items.Key;
import cz.cuni.mff.abacs.burglar.logics.objects.positions.BasePosition;
import cz.cuni.mff.abacs.burglar.logics.objects.positions.Door;
import cz.cuni.mff.abacs.burglar.logics.objects.positions.Lockable;
import cz.cuni.mff.abacs.burglar.logics.objects.positions.Position;
import cz.cuni.mff.abacs.burglar.logics.planning.instructions.Instruction;


/** 
 * A game map that executes instructions on it's agents.
 * 
 * @author abacs
 *
 */
public class ExecutingMap extends DataMap implements GameMap {
	// properties:
	
	
	// -------------------------------------------------------------------------
	// constructors:
	
	
	/**
	 * 
	 * 
	 * @param nextFreeId
	 */
	public ExecutingMap(int nextFreeId, int requiredTrapRoomCount) {
		super(nextFreeId, requiredTrapRoomCount);
	}

	
	// -------------------------------------------------------------------------
	
	
	/**
	 * 
	 * 
	 * @return
	 */
	public List<Node> getNodes() {
		List<Node> nodes = new LinkedList<Node>();
		for(Position pos : this.getPositions()){
			nodes.add(((BasePosition)pos).getNode());
		}
		return nodes;
	}
	
	
	// -------------------------------------------------------------------------
	
	
	public void addGuardPatrol(List<Integer> roomIds) {
		Position position = this.getFloorPositions(roomIds.get(0)).get(0);
		Guard guard = new Guard(this.getNextID(), position, roomIds, this);
		
		BeliefBase knowledge = guard.getBeliefBase();
		knowledge.seenFromNear(this.getOperablePositions());
		
		this.addAgent(guard);
	}
	
	
	// -------------------------------------------------------------------------
	
	
	/**
	 * The instructions are translated to simple instructions if needed.
	 * 
	 * Unchecked function. Agent code must be correct.
	 * 
	 * 
	 */
	@Override
	public void addInstructions(List<Instruction> instructions) {
		// generate simple instructions if needed:
		List<Instruction> simpleInstructions = 
			this.generateSimpleInstructions(instructions);
		
		// clear the agent isnstructions that are in the instructionlist:
		List<Integer> agents = new LinkedList<Integer>();
		for(Instruction instr : simpleInstructions){
			if(agents.contains(instr._agentId) == false){
				agents.add(instr._agentId);
			}
		}
		for(int agentId : agents)
			this.getAgent(agentId).clearInstructions();
		
		
		// add instructions to their executing agent:
		for(Instruction instr : simpleInstructions){
			assert(this.getAgent(instr._agentId) != null) : "invalid Agent id in Instruction"; 
			this.getAgent(instr._agentId).addInstruction(instr);
		}
	}
	
	
	@Override
	public void clearInstructions() {
		for(Agent agent : this.getAgents()){
			agent.clearInstructions();
		}
	}
	
	
	@Override
	public List<Instruction> getAgentInstructions(int agentId) {
		return this.getAgent(agentId).getInstructions();
	}
	

	@Override
	public List<Instruction> getAgentInstructions(Agent agent) {
		return agent.getInstructions();
	}
	
	
	/**
	 * Returns the first instruction for a selected agent. 
	 * It also removes it from the main list.
	 * 
	 * @param agentId
	 * @return
	 */
	protected Instruction popFirstInstructionForAgent(int agentId) {
		return this.getAgent(agentId).popFirstInstruction();
	}
	
	
	/**
	 * Returns the first instruction for a selected agent. 
	 * It also removes it from the main list.
	 * 
	 * @param agent
	 * @return
	 */
	protected Instruction popFirstAgentInstruction(Agent agent) {
		return agent.popFirstInstruction();
	}
	
	
	@Override
	public boolean instructionListsMatch(List<Instruction> a, List<Instruction> b) {
		
		System.out.println("sizes" + a.size() +" ? " + b.size());
		
		if(a.size() != b.size()){
			return false;
		}
		
		for(int i = 0; i < a.size(); i++){
			Instruction instrA = a.get(i);
			Instruction instrB = b.get(i);
			if(instrA.matches(instrB) == false)
				return false;
		}
		
		return true;
	}
	
	
	@Override
	public List<Room> instructionsLeadToTraps(List<Instruction> list) {
		List<Room> trapRooms = this.getTrapRooms();
		int burglarId = this.getBurglar().getId();
		
		List<Room> triggeredRooms = new ArrayList<Room>();
		
		for(Instruction instr : list){
			// if the agent is the burglar and the current room contains a trap
			// the room is added to the triggered trap list
			// all room can be only once in the triggered list.
			
			if(instr._agentId != burglarId)
				continue;
			
			Position position = this.getPosition(instr._subjectId);
			
			if(position.isTypeOf(BaseInterface.Type.DOOR)){
				Door door = (Door)position;
				Room rooms[] = door.getRooms();
				
				if(trapRooms.contains(rooms[0]) &&
				   triggeredRooms.contains(rooms[0]) == false)
					triggeredRooms.add(rooms[0]);
				if(trapRooms.contains(rooms[1]) &&
				   triggeredRooms.contains(rooms[1]) == false)
					triggeredRooms.add(rooms[1]);
			}else{
				Room room = position.getRoom();
				if(trapRooms.contains(room) &&
				   triggeredRooms.contains(room) == false)
					triggeredRooms.add(room);
			}
		}
		
		return triggeredRooms;
	}
	
	
	// -------------------------------------------------------------------------
	
	
	/** 
	 * Executes a single step for each agent.
	 * 
	 * @return <code>true</code> the burglar had at least one action
	 * 			left to complete
	 */
	public StepResult executeStep() {
		StepResult ret = StepResult.NOTHING;
		for(Agent agent : this.getAgents()){
			StepResult agentRet;
			if(agent.isTypeOf(BaseInterface.Type.BURGLAR)){
				agentRet = this.execute(agent.popFirstInstruction());
			}else{
				Instruction instr = agent.popFirstInstruction();
				agentRet = this.execute(instr);
				agent.addInstruction(instr);
			}
			
			if(agentRet != StepResult.NOTHING && ret != StepResult.IMPORTANT_CHANGE)
				ret = agentRet;
		}
		return ret;
	}
	
	
	/** 
	 * Executes a single instruction.
	 * 
	 * Important change occurs when the agent enters a new room, 
	 * or the action fails.
	 * 
	 * @param instr
	 * @return 
	 */
	public StepResult execute(Instruction instr) {
		if(instr == null)
			return StepResult.FINISHED;
		StepResult ret = StepResult.NOTHING;
		
		Agent agent = this.getAgent(instr._agentId);
		assert(agent != null);
		
		switch(instr._code){
		case MOVE:
			// there is an important change if the room was changed or 
			// if the agent was not successful.
			Room room = agent.getRoom();
			if(this.move(instr._agentId, instr._subjectId) == false ||
			   agent.getRoom() != room){
				ret = StepResult.IMPORTANT_CHANGE;
			}
			break;
		case OPEN:
			if(this.open(instr._agentId, instr._subjectId) == false)
				ret = StepResult.IMPORTANT_CHANGE;
			break;
		case CLOSE:
			this.close(instr._agentId, instr._subjectId);
			break;
		case UNLOCK:
			if(this.unlock(instr._agentId, instr._subjectId, instr._itemId) == false)
				ret = StepResult.IMPORTANT_CHANGE;
			break;
		case LOCK:
			if(this.lock(instr._agentId, instr._subjectId, instr._itemId) == false)
				ret = StepResult.IMPORTANT_CHANGE;
			break;
		case PICK_UP:
			if(this.pickUp(instr._agentId, instr._subjectId, instr._itemId) == false)
				ret = StepResult.IMPORTANT_CHANGE;
			break;
		}
		return ret;
	}
	
	
	// -------------------------------------------------------------------------
	
	
	@Override
	public List<Position> nodesToPositions(List<Node> nodes) {
		List<Position> positions = new ArrayList<Position>();
		for(Node node : nodes)
			positions.add(this.getPosition(node));
		return positions;
	}
	
	
	// -------------------------------------------------------------------------
	
	
	/**
	 * (Has to run on a single agent)
	 */
	@SuppressWarnings("unchecked")
	public List<Instruction> generateSimpleInstructions(
			List<Instruction> instructions
	) {
		List<Instruction> result = new ArrayList<Instruction>();
		
		Position position = null;
		
		for(Instruction instr : instructions){
			if(instr.isComplexInstruction()){
				
				Agent agent = this.getAgent(instr._agentId);
				
				if(position == null){
					position = agent.getPosition();
				}
				
				if(instr._code == Instruction.code.ENTER_DOOR){
					Instruction simpleInstr = 
						new Instruction(
								Instruction.code.MOVE,
								instr._agentId,
								instr._subjectId
						);
					result.add(simpleInstr);
					position = this.getPosition(instr._subjectId);
				}
				
				if(instr._code == Instruction.code.COMPLEX_MOVE &&
					instr._subjectId != position.getId()){
					Position aim = this.getPosition(instr._subjectId);
					
					
					AStar aStar = new AStar();
					aStar.nodes.addAll(this.getNodes());
					
					List<Node> nodes = 
						aStar.getPath(
								((BasePosition)position).getNode(),
								((BasePosition)aim).getNode());
					
					// if the last position to visit wasn't the aim, it means, 
					// it was an unreachable position, 
					// we need to include the last step to it.
					int includeStepsUntill = 1;
					if((nodes.get(0)) != ((BasePosition)aim).getNode())
						includeStepsUntill = 0;
					
					for(int index = nodes.size() - 2;
						index >= includeStepsUntill;
						index--
					){
						position = this.getPosition(nodes.get(index));
						Instruction simpleInstr = 
							new Instruction(
									Instruction.code.MOVE,
									instr._agentId,
									position.getId()
							);
						result.add(simpleInstr);
					}
				}
				
			}else{
				result.add(instr);
			}
		}
			
		return result;
	}
	
	
	// -------------------------------------------------------------------------
	
	
	@Override
	public boolean isBurglarTrapped() {
		if(this.getTrapRooms().contains(this.getBurglar().getRoom()))
			return true;
		
		for(Guard guard : this.getGuards()){
			System.out.println("guard ? burglar : "+guard.getRoomId()+" ? "+this.getBurglar().getRoomId());
			
			if(guard.getRoomId() == this.getBurglar().getRoomId())
				return true;
		}
		return false;
	}
	
	
	public boolean willBurglarBeCought() {
		List<Integer> roomIds = this.getTrapRoomIds();
		
		for(int id : this.getBurglar().getPlannedRoomIds()){
			if(roomIds.contains(id))
				return true;
		}
		
		return false;
	}
	
	
	// -------------------------------------------------------------------------
	
	
	private boolean move(int agentId, int posId) {
		return this.move(this.getAgent(agentId), this.getPosition(posId));
	}
	
	private boolean move(Agent agent, Position pos) {
		return agent.moveTo(pos);
	}
	
	
	private boolean open(int agentId, int lockableId) {
		return this.open(
				this.getAgent(agentId),
				(Lockable)this.getPosition(lockableId)
		);
	}
	
	
	private boolean open(Agent agent, Lockable lockable) {
		return agent.open(lockable);
	}
	
	
	private boolean close(int agentId, int lockableId) {
		return this.close(
				this.getAgent(agentId),
				(Lockable) this.getPosition(lockableId)
		);
	}
	
	
	private boolean close(Agent agent, Lockable lockable) {
		return agent.close(lockable);
	}
	
	
	private boolean unlock(int agentId, int lockableId, int keyId) {
		return this.unlock(
				this.getAgent(agentId),
				(Lockable) this.getPosition(lockableId),
				(Key) this.getItem(keyId)
		);
	}
	
	
	private boolean unlock(Agent agent, Lockable lockable, Key key) {
		return agent.unlock(lockable, key);
	}
	
	
	private boolean lock(int agentId, int lockableId, int keyId) {
		return this.lock(
				this.getAgent(agentId),
				(Lockable) this.getPosition(lockableId),
				(Key) this.getItem(keyId)
		);
	}
	
	
	private boolean lock(Agent agent, Lockable lockable, Key key) {
		return agent.lock(lockable, key);
	}
	
	
	private boolean pickUp(int agentId, int inventoryId, int itemId) {
		return this.pickUp(
				this.getAgent(agentId),
				(Inventory) this.getPosition(inventoryId),
				this.getItem(itemId)
		);
	}
	
	
	private boolean pickUp(Agent agent, Inventory inventory, Item item) {
		return agent.pickUp(inventory, item);
	}
	
	
}
